iT邦幫忙

2024 iThome 鐵人賽

DAY 9
2

全域單例狀態管理

什麼是單例狀態?

開始講到單例狀態就要稍微舉例一下什麼是單例狀態,其實就是泛指所有 Singleton 的物件。舉凡整個應用程式只有唯一的狀態,便都是這種狀態。比如「History Router」、「Locale Language」、「Browser Storage」都算是。這類的狀態不但可以進行微前端溝通,還會產生副作用。如果沒有良好的控制管理,都會引發許多狀態管理的麻煩。

單例狀態有什麼管理議題?

單例狀態之所以是單例狀態,就是整個 global 之下只有一個 因為你會發現很容易發生「循環修改」,資料流的混亂。你根本搞不清楚誰先誰後,誰頭誰尾。

應該怎麼處理?

單向資料流

要控制資料流並不簡單,因為很容易發生資料被到處修改的問題,你也很難管理團隊或套件去修改 global browser API。但能做的基本還是要做,建立狀態的抽象介面,統一 Side Effect 的資料流流向,這才能最好的去控制資料能夠集中。

// router.js
export function navigation(path) {
  history.pushState({}, "", path);
  window.dispatchEvent(new Event("navigation"));
}
import { navigation } from "./router";

function RouterLink() {
  const onClick = (path) => {
    navigation(path);
  };
  return <button onClick={onClick} />;
}

這樣整個專案都可以透過 navigation 來達到改變 history 的狀態,也可以使用 dispatchEvent 去和不同的微前端應用程式進行同步。

但終究是微前端,多個應用下資料依然是不同步,並不是完美的單向資料流。此時可以利用 Event 製作一個主動觸發的行為,形成一個單一 Provider 的事件觸發器。

// init.js
export function init() {
  window.addEventListener("navigation", (event) => {
    history.pushState({}, "", event.detail.path);
  });
}
// router.js
export function navigation(path) {
  window.dispatchEvent(
    new CustomEvent("navigation", {
      detail: { path },
    })
  );
}
import { navigation } from "./router";

function RouterLink() {
  const onClick = (path) => {
    navigation(path);
  };
  return <button onClick={onClick} />;
}

這樣只要確保 init 在應用程式初始化時只執行一次,那全部的微前端都可以使用 navigation 方法來進行 Side Effect。

攔截同步

又舉例,如果想要攔截本身 window 自帶的方法怎麼辦?

const originOpen = window.open;

window.open = function (...args) {
  originOpen.apply(this, args);
  // implements
};
const prototypePush = Array.prototype.push;

Array.prototype.push = function (...args) {
  prototypePush.apply(this, args);
  // implements
};

透過替換掉原本的方法,這樣就可以控管攔截後的結果,如此一來就可以對一些 Library 底層行為進行攔截或改變。

路由器

在微前端架構中,路由器(Router)是不得不面對,而且情境極複雜的一個議題。

因為 browser history 的對象在整個前端應用程式是唯一的,不會存在第二個。但如果有多的前端應用程式產生出多個 history 的操作器,那高機率就要打架了。在整個運作系統之中,最怕的就是「改了不動」、「改了不知道」、「遞迴循環修改」等情境。

其實看過各大框架系統,都不太推薦微前端模組另外開自己的路由系統,多半鼓勵以「原子化元件」作為切分點。如果以元件切分,確實路由議題可以全然仰賴上層主應用控制,它本身的渲染控制便不在元件邏輯內。

但相信做過微前端的都知道,你每個微前端切分成本很大,會希望一個應用包裝更多功能而不要切分,這就延伸新的問題「路由器的狀態同步」。狀態同步議題其實回到上一篇講的「單例狀態管理」,解決方案是一致的。

我這邊最推薦的就是微前端應用「不要操作路由器」,那不操作路由器該怎麼操作與通知?其實是一樣的解法,採用單向資料流的方式統一管理,讓可以改變狀態的窗口全部一致在某個觸發點,不能全世界好幾個地方能改。當單向資料流徹底管理後,就可以很容易去攔截所有路由的副作用,如此一來就可以建構狀態管理系統去「取得」、「修改」、「監聽」,行形成一套資料流。

HttpFetcher

先說明 HttpFetcher 是什麼。當前主流的 HttpFetcher 應該就是 axios, fetch ... 等等方式,凡是向網路發出請求的方法都是 HttpFetcher 相關的工具。

有什麼問題?

大部分我們在處理 Http Request 時,都會有一些「固定要做的事」。精準來說要舉例就是一些共同的處理,比如說將 Token 注入、請求的錯誤處理、回應的錯誤處理、回應的處理等等,相關的中介層行為肯定不會想重複編寫,大部分行為對於一個網站也幾乎是一致的,不太會有不同頁有不同的處理。所以在微前端的架構如何去共用這個行為呢?

解決方案

建立 Client Service

既然是 HTTP 中介層,使用的處理行為就未必要在瀏覽器進行完成,你可以選擇在伺服器進行請求處理,如 Token 管理就可以在伺服器端完成。

連段式 Event

透過層層事件把行為透過 EventBus 傳遞給每一層事件處理,算是另類的 middleware,有點 Chain of Responsibility Pattern 的感覺。除了可以用 deep 的方式去深層傳遞,也可以用 Next Event 的手法去幫助層層傳遞。EventBus 更方便讓微前端之間進行溝通與攔截,達到更高的自由度。

建立共用 Instance

這方式就是建立一個抽象實體去讓全部請求共用,管理這一份記憶體也可以很好控制所有 Interceptor 行為。

Interceptor Service

強行攔截所有 HTTP 請求,重新封裝 XHR 與 Fetch 行為,在 ServiceWork 重新處理請求行為。

不考慮共用

直接放棄所有共用行為,每一個微前端應用程式都獨立執行對應的行為。

結論

其實全域狀態還有很多,方案也許也很多,我也只提供初步的作法,事實上要優化的部分還非常多,礙於篇幅就不特別再多說。


上一篇
(八) 微前端的溝通
下一篇
(十) 樣式管理問題
系列文
前端也可以搞微服務?!前端最複雜的一種架構30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言